Within this notebook, you'll learn the basics of how to use folium to generate maps with custom layers layers are a visual overlay that can be placed on a map, often based on a map location to provide detailed information about an area
We will also be using the Nominatim REST API through the geopy library to easily access open-source geocoding data based on OpenStreetMap This project can easily be used in any type of environment outside of python through setting up a flask server
When using Nominatim, Please look at the usage policy before using: https://operations.osmfoundation.org/policies/nominatim/
First we will need to import our required libraries. If you haven't already remember to install all dependencies in side of the requirements.txt file [pip i requirements.txt]
import pandas as pd
import folium
from pprint import pprint
from geopy.geocoders import Nominatim
For our first example we will highlight the location of calgary on a map and adding a marker for the UofC. First we will need to set up our geopy library to have the proper user-agent for the api to authenticate
geolocator = Nominatim(user_agent="http")
Lets start a new Query where we want to get information on Calgary and more specifically, we want to get the shape of it
location = geolocator.geocode("Calgary AB Canada", geometry='geoJson')
We can print out the response from the api with .raw
pprint(location.raw)
{'boundingbox': ['50.842526', '51.2125013', '-114.3157587', '-113.8600018'],
'class': 'boundary',
'display_name': 'Calgary, Alberta, Canada',
'geojson': {'coordinates': [[[-114.3157587, 51.1397612],
[-114.3156155, 51.1396239],
....
[-114.3041835, 51.1396886],
[-114.3157587, 51.1397612]]],
'type': 'Polygon'},
'icon': 'https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png',
'importance': 0.8614801395324394,
'lat': '51.0460954',
'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '
'https://osm.org/copyright',
'lon': '-114.065465',
'osm_id': 3227127,
'osm_type': 'relation',
'place_id': 282785438,
'type': 'administrative'}
Here we can see that we get lots of information about the query that we made in json format. We want to get the shape of calgary so we will be looking at geojson > coordinates Here we get a list of a list with the positional coordinates of each polygon point in [[long, lat],... ] pairings
For our mapping tool we will be using folium which takes polygons in with a different format [(lat, long),... ] so lets clean up our data to have that pairing format
If we look again at the result we got from nominatim, we can see that theres another dimension to the geoJson. This is because a query can result in multiple disconnected polygons that will represent the same place. For example: the islands east of BC. Since Calgary Alberta is a single connected location we don't need to worry about that and can just take the first and only element
geoJson = location.raw['geojson']['coordinates'][0]
# format data from [[],[]...] to [(),(),...]
geoJson = [(x[1],x[0]) for x in geoJson]
pprint(geoJson)
[(51.1397612, -114.3157587), (51.1396239, -114.3156155), ... (51.1396886, -114.3041835), (51.1397612, -114.3157587)]
Perfect our data is cleaned up and ready to be used in a map.
To initalize our map, we will call the factory map method inside of folium and store that as a variable. At any time, we can call that variable to display our map inside of a jupyter notebook. [In order to draw an image outside of jupyter, you'll need to save it to a file. This can be done with the .save("<filename.html>") function. You can then open it in a browser to view it]
We will give this factory method the starting location that we want the map to be zoomed into and its initial zoom level
my_map = folium.Map(location=[51.0486, -114.0708], zoom_start=11)
Lets start drawing our custom layers. We will want a layer to highlight the geometry of calgary and also a seperate layer to display the location of its university.
With folium we can create a Feature Group, which we can add our overlays too.
After making our feature group, we can then bind it to our map.
In order to control/toggle layers we will add a layer controller to the map
city_layer = folium.FeatureGroup(name="Calgary")
city_layer.add_to(my_map)
university_layer = folium.FeatureGroup(name="University")
university_layer.add_to(my_map)
folium.LayerControl().add_to(my_map)
<folium.map.LayerControl at 0x1dc63a86440>
Now we can start drawing on our map! to start, lets draw the geometry for calgary with the polygon tool in folium. We will supply it with the geoJson that we just cleaned, a stroke color, stroke weight, fill colour and opacity. We don't want to draw this directly onto the map, since we want to toggle it, so we will be adding it to our respective feature layer
# generate polygon for calgary
folium.Polygon(geoJson,
color='red',
weight=5,
fill=True,
fill_color='green',
fill_opacity=0.6).add_to(city_layer)
<folium.vector_layers.Polygon at 0x1dc63a87a30>
Next lets add a marker for the university on the map. We will once again be using Nominatim to query for the University of calgary
# add marker for University of Calgary
location = geolocator.geocode("University Of Calgary")
pprint(location.raw)
folium.Marker(location=[location.latitude, location.longitude],
popup="<h1>University of Calgary</h1>",
icon=folium.Icon(color='blue', icon='info-sign')).add_to(university_layer)
folium.CircleMarker(location=[location.latitude, location.longitude],
radius=50,
popup="<h1>University of Calgary</h1>",
color='blue',
fill=True,
fill_color='blue').add_to(university_layer)
{'boundingbox': ['51.0783151', '51.0784151', '-114.1283572', '-114.1282572'],
'class': 'amenity',
'display_name': 'University of Calgary, 500 Campus Place NW, Charleswood, '
'Calgary, Alberta, T2N 1N7, Canada',
'icon': 'https://nominatim.openstreetmap.org/ui/mapicons//education_university.p.20.png',
'importance': 0.31100000000000005,
'lat': '51.0783651',
'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '
'https://osm.org/copyright',
'lon': '-114.1283072',
'osm_id': 29023292,
'osm_type': 'node',
'place_id': 111865,
'type': 'university'}
<folium.vector_layers.CircleMarker at 0x1dc63a844f0>
my_map.save('map.html')
my_map
Provinces = ["British Columbia", "Alberta", "Saskatchewan", "Manitoba", "Ontario", "Quebec", "New Brunswick", "Nova Scotia", "Prince Edward Island", "Newfoundland and Labrador", "Northwest Territories", "Nunavut", "Yukon"]
Country = "Canada"
df = pd.DataFrame(columns=["Location Name", "Latitude", "Longitude", "geometry", "weather"])
for province in Provinces:
row = {"Location Name": province + ', ' + Country, "Latitude": 0, "Longitude": 0, "geometry": None, "weather": None}
# add to the end of the dataframe
df = pd.concat([df, pd.DataFrame(row, index=[len(df)])])
# loop through all provinces on df and append the geometry to the geometries list
for index, row in df.iterrows():
location = geolocator.geocode(row["Location Name"], geometry='geoJson')
if location is not None:
row["Latitude"] = location.latitude
row["Longitude"] = location.longitude
geoJson = location.raw['geojson']['coordinates'][0]
row["geometry"] = geoJson
# add random weather data to the df
import random
for index, row in df.iterrows():
row["weather"] = random.randint(-30,30)
canada_map = folium.Map(location=[51.0486, -114.0708], zoom_start=3)
feature_group_dictionary = {}
for index, row in df.iterrows():
if row["Location Name"] not in feature_group_dictionary:
feature_group_dictionary[row["Location Name"]] = folium.FeatureGroup(name=row["Location Name"])
feature_group_dictionary[row["Location Name"]].add_to(canada_map)
for indew, row in df.iterrows():
if row["geometry"] is not None:
geoJson = row["geometry"]
if type(geoJson[0][0]) == list:
for island in geoJson:
island = [(x[1],x[0]) for x in island]
row["geometry"] = island
folium.Polygon( island,
color='red',
weight=5,
fill=True,
fill_color='red' if row['weather'] > 0 else 'blue',
fill_opacity=abs(row['weather'])/(30 + 20)).add_to(feature_group_dictionary[row["Location Name"]])
else:
# format data from [[],[]...] to [(),(),...]
geoJson = [(x[1],x[0]) for x in geoJson]
# update the row with the new geometry
row["geometry"] = geoJson
folium.Polygon( geoJson,
color='red',
weight=5,
fill=True,
fill_color='red' if row['weather'] > 0 else 'blue',
fill_opacity=abs(row['weather'])/(30 + 20)).add_to(feature_group_dictionary[row["Location Name"]])
# add marker for that province
popup = folium.Popup(str(row['Location Name']) + "<br>" + str(row["weather"]) + '°C', max_width=500)
folium.Marker(location=[row["Latitude"], row["Longitude"]],
popup=popup,
icon=folium.Icon(color='blue', icon='info-sign')).add_to(feature_group_dictionary[row["Location Name"]])
folium.LayerControl().add_to(canada_map)
pprint(df)
canada_map
Location Name Latitude Longitude \
0 British Columbia, Canada 55.001251 -125.002441
1 Alberta, Canada 55.001251 -115.002136
2 Saskatchewan, Canada 55.532126 -106.141224
3 Manitoba, Canada 55.001251 -97.001038
4 Ontario, Canada 50.000678 -86.000977
5 Quebec, Canada 52.476089 -71.825867
6 New Brunswick, Canada 46.500283 -66.750183
7 Nova Scotia, Canada 45.19604 -63.165379
8 Prince Edward Island, Canada 46.503545 -63.595517
9 Newfoundland and Labrador, Canada 53.821733 -61.229553
10 Northwest Territories, Canada 65.0 -118.0
11 Nunavut, Canada 65.037773 -92.554079
12 Yukon, Canada 63.000147 -136.002502
geometry weather
0 [(59.9996374, -139.0613278), (59.9948369, -139... 10
1 [(54.9957051, -120.0013835), (54.9254324, -120... 1
2 [(59.9996139, -110.006368), (59.6484782, -110.... 16
3 [(59.8017646, -102.0075806), (59.7757516, -102... 19
4 [(52.6236382, -95.1537399), (52.6233596, -95.1... -5
5 [(54.63156, -79.7619499), (54.6309, -79.75954)... -17
6 [(47.2971796, -69.0534663), (47.296441, -69.05... -9
7 [(44.245, -66.6779952), (44.241, -66.677902), ... 17
8 [(46.6811101, -64.4137999), (46.6809463, -64.4... 2
9 [(54.0257023, -67.8216853), (54.0234152, -67.8... -1
10 [(69.4031067, -136.588001), (68.9891294, -136.... -6
11 [(68.0002875, -120.6828104), (67.9825881, -120... 24
12 [(69.7041667, -141.00275), (69.703632, -141.00... -24